AI Movement AI Scriptable Object AI Vision Cone Sound Barrier

AI Movement

Summary
I created an AI for my third person game to roam on parts of the map and attack the player. I completed this by using the Unity engines NavMeshAgent. This built in asset is great to create AI en pathing on the map. For this script I tried to keep the code as minimal as possible. I completed this by using the quickest ways to call or return stuff and using scriptable objects for the AI settings.

Movement
We start off by creating a NavMeshAgent variable, all values of the AI itself are already stored in, like the height, width of the AI. I then create an enum that stores all states of the AI. So for now I use idle, roam, find and attack. Most of these states speak for themselfs.

- Idle: Stands still but still has the ability to look around in the area.

- Roam: Walk around the walkable area trying to find the player.

- Attack: When the player is in the AI it's vision it goes to the players exact position to attack him.

- Find/LastPos: When the player escape the AI by moving around a corner so the AI loses it's player vision, the AI goes to the last position the player was in still being able to find the player around the corner.

This all seems smaal for an AI, but since it's just the minimal code it's always expendable.

using UnityEngine.AI;
using UnityEditor;

public enum AgentState
{
	idle = 0,
	roam,
	find,
	attack
}

[RequireComponent(typeof(NavMeshAgent))]
public class AIMovement : MonoBehaviour
{
	private float idleTime = 2;

	[Header("AI State")]
	public AgentState state;

	[Space(20)]
	[Header("Agent Info")]
	[SerializeField] private NavMeshAgent agent;

	[Space(20)]
	[Header("AI Settings")]
	public AISO AIStats;

	[HideInInspector]
	public Vector3 GoToPos;

	void Start()
	{
		if (agent == null)
			agent = GetComponent();
	}

	void Update()
	{
		switch (state)
		{
			case AgentState>.idle:
				Idle();
				break;
			case AgentState>.roam:
				Roam();
				break;
			case AgentState>.find:
				LastPos();
				break;
			case AgentState>.attack:
				Attack();
				break;
			default:
				break;
		}
	}

	private void Idle()
	{
		transform.Rotate(new Vector3(0, 20, 0), 100 * Time.deltaTime);

		idleTime -= Time.deltaTime;
			if (idleTime <= 0)
		{

			int minplus = Random.Range(0, 2);
			print(minplus);

				if (minplus == 0)
				agent.SetDestination(new Vector3(transform.position.x + Random.Range(AIStats.MinRoamRange, AIStats.MaxRoamRange), transform.position.y, transform.position.z + Random.Range(AIStats.MinRoamRange, AIStats.MaxRoamRange)));
			else if (minplus == 1)
				agent.SetDestination(new Vector3(transform.position.x - Random.Range(AIStats.MinRoamRange, AIStats.MaxRoamRange), transform.position.y, transform.position.z - Random.Range(AIStats.MinRoamRange, AIStats.MaxRoamRange)));

			state = AgentState.roam;

			idleTime = Random.Range(1, 2.5f);
		}
	}

	private void Roam()
	{
			if (agent.pathPending || agent.remainingDistance > 0.1f)
			return;

		state = AgentState.idle;
	}

	private void LastPos()
	{
			if (agent.pathPending || agent.remainingDistance < 0.1f)
			state = AgentState.roam;

		agent.SetDestination(GoToPos);
	}

	private void Attack()
	{
			if (agent.pathPending || agent.remainingDistance < 0.1f)
			state = AgentState.idle;

		agent.SetDestination(GoToPos);
	}

	private void OnDrawGizmosSelected()
	{
		Gizmos.color = Color.blue;
		Gizmos.DrawLine(transform.position, agent.destination);
	}
}

AI Scriptable Object

AISO
AISO stands for
(Artificial Intelligence Scriptable Object). I created this scriptable object so I can create a variety of different enemies with different speed and distances.

So all information is stored in the scriptable object of the AI and AIMovement can call these variables.

[CreateAssetMenu(fileName = "AIData", menuName = "AIStats", order = 0)]
public class AISO : ScriptableObject
{
	[Header("Speed Settings")]
	public float RoamSpeed = 2;
	public float MinRoamRange = 5;
	public float MaxRoamRange = 25;
	public float AttackSpeed = 3;

	[Space(20)]
	[Header("Vision Cone Settings")]
	[Range(0, 100)] public float ViewRadius = 25;
	[Range(0, 360)] public float ViewAngle = 90;

	[Space(10)]
	[Header("Layer Settings")]
	public LayerMask LookForLayer;
	public LayerMask ObstacleLayer;
}

AI Vision Cone

Vision Cone
With the help of Sebastion Lague I was able to create a advanced vision cone. This vision Cone works great for visualization on what the AI sees.

What happens in the script is that it uses a mesh and basically cuts it where it's not being seen by the AI. Also this AI vision cone is easily reusable for games with multiple target as you can see in the FindVisibleTargets() function. But what I did is with one player and get that players position for the AI to attack. So the AI sees the player in it's vision cone and will enter it's own component AIMovement and gets the GoToPos variable. Also this FindVisible() function is updated with a delay so it doesn't enter the scripts values every frame.

For the vision cone settings of the AI I entered the AI scriptable object to get the radius and angle of the AI.

This vision cone script does a lot of calculation for the mesh, since if you remove the calculations for the cutting the mesh would just go straight through every object.

In the codebox below I created the FieldOfViewEditor script to have the vision cone appear on the screen and be customized as custom editor.

[RequireComponent(typeof(AIMovement))]
public class FieldOfView : MonoBehaviour
{
	private AIMovement AI;

	[HideInInspector] public float ViewRadius;
	[HideInInspector] public float ViewAngle;

	private LayerMask TargetMask;
	private LayerMask ObstacleMask;

	[HideInInspector]
	public List<Transform> VisibleTargets = new List<Transform>();

	[SerializeField] private float MeshResolution;
	[SerializeField] private int EdgeResolveIterations;
	[SerializeField] private float EdgeDstThreshold;

	public MeshFilter ViewMeshFilter;
	Mesh viewMesh;

	public Vector3 TargetPos;

    private void Awake()
    {
		AI = GetComponent<AIMovement>();
    }

    void Start()
	{
		viewMesh = new Mesh();
		viewMesh.name = "View Mesh";
		ViewMeshFilter.mesh = viewMesh;

		StartCoroutine("FindTargetsWithDelay", .2f);
	}

    private void FixedUpdate()
    {
		SetSettings();
    }

    IEnumerator FindTargetsWithDelay(float delay)
	{
		while (true)
		{
		yield return new WaitForSeconds(delay);
			FindVisibleTargets();
		}
	}

	void LateUpdate()
	{
		DrawFieldOfView();
	}

	private void SetSettings()
    {
        	if (ViewRadius != AI.AIStats.ViewRadius)
			ViewRadius = AI.AIStats.ViewRadius;

		if (ViewAngle != AI.AIStats.ViewAngle)
			ViewAngle = AI.AIStats.ViewAngle;

		if (TargetMask != AI.AIStats.LookForLayer)
			TargetMask = AI.AIStats.LookForLayer;

		if (ObstacleMask != AI.AIStats.ObstacleLayer)
			ObstacleMask = AI.AIStats.ObstacleLayer;
	}

	private void FindVisibleTargets()
	{
		VisibleTargets.Clear();
		Collider[] targetsInViewRadius = Physics.OverlapSphere(transform.position, ViewRadius, TargetMask);

		for (int i = 0; i < targetsInViewRadius.Length; i++)
		{
			Transform target = targetsInViewRadius[i].transform;
			Vector3 dirToTarget = (target.position - transform.position).normalized;
			if (Vector3.Angle(transform.forward, dirToTarget) < ViewAngle / 2)
			{
				float dstToTarget = Vector3.Distance(transform.position, target.position);
				if (!Physics.Raycast(transform.position, dirToTarget, dstToTarget, ObstacleMask))
				{
					VisibleTargets.Add(target);
					AI.GoToPos = new Vector3(target.position.x, target.position.y, target.position.z);
					AI.state = AgentState.attack;
				}
			}
		}
	}

	void DrawFieldOfView()
	{
		int stepCount = Mathf.RoundToInt(ViewAngle * MeshResolution);
		float stepAngleSize = ViewAngle / stepCount;
		List<Vector3> viewPoints = new List<Vector3>();
		ViewCastInfo oldViewCast = new ViewCastInfo();
		for (int i = 0; i <= stepCount; i++)
		{
			float angle = transform.eulerAngles.y - ViewAngle / 2 + stepAngleSize * i;
			ViewCastInfo newViewCast = ViewCast(angle);

			if (i > 0)
			{
				bool edgeDstThresholdExceeded = Mathf.Abs(oldViewCast.dst - newViewCast.dst) > EdgeDstThreshold;
				if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && edgeDstThresholdExceeded))
				{
					EdgeInfo edge = FindEdge(oldViewCast, newViewCast);
					if (edge.pointA != Vector3.zero)
					{
						viewPoints.Add(edge.pointA);
					}
					if (edge.pointB != Vector3.zero)
					{
						viewPoints.Add(edge.pointB);
					}
				}
			}

			viewPoints.Add(newViewCast.point);
			oldViewCast = newViewCast;
		}

		int vertexCount = viewPoints.Count + 1;
		Vector3[] vertices = new Vector3[vertexCount];
		int[] triangles = new int[(vertexCount - 2) * 3];

		vertices[0] = Vector3.zero;
		for (int i = 0; i < vertexCount - 1; i++)
		{
			vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i]);

			if (i < vertexCount - 2)
			{
				triangles[i * 3] = 0;
				triangles[i * 3 + 1] = i + 1;
				triangles[i * 3 + 2] = i + 2;
			}
		}

		viewMesh.Clear();

		viewMesh.vertices = vertices;
		viewMesh.triangles = triangles;
		viewMesh.RecalculateNormals();
	}


	EdgeInfo FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewCast)
	{
		float minAngle = minViewCast.angle;
		float maxAngle = maxViewCast.angle;
		Vector3 minPoint = Vector3.zero;
		Vector3 maxPoint = Vector3.zero;

		for (int i = 0; i < EdgeResolveIterations; i++)
		{
			float angle = (minAngle + maxAngle) / 2;
			ViewCastInfo newViewCast = ViewCast(angle);

			bool edgeDstThresholdExceeded = Mathf.Abs(minViewCast.dst - newViewCast.dst) > EdgeDstThreshold;
			if (newViewCast.hit == minViewCast.hit && !edgeDstThresholdExceeded)
			{
				minAngle = angle;
				minPoint = newViewCast.point;
			}
			else
			{
				maxAngle = angle;
				maxPoint = newViewCast.point;
			}
		}

		return new EdgeInfo(minPoint, maxPoint);
	}


	ViewCastInfo ViewCast(float globalAngle)
	{
		Vector3 dir = DirFromAngle(globalAngle, true);
		RaycastHit hit;

		if (Physics.Raycast(transform.position, dir,	out hit, ViewRadius, ObstacleMask))
		{
			return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);
		}
		else
		{
			return new ViewCastInfo(false, transform.position + dir * ViewRadius, ViewRadius, globalAngle);
		}
	}

	public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
	{
		if (!angleIsGlobal)
		{
			angleInDegrees += transform.eulerAngles.y;
		}
		return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
	}

	public struct ViewCastInfo
	{
		public bool hit;
		public Vector3 point;
		public float dst;
		public float angle;

		public ViewCastInfo(bool _hit, Vector3 _point, float _dst, float _angle)
		{
			hit = _hit;
			point = _point;
			dst = _dst;
			angle = _angle;
		}
	}

	public struct EdgeInfo
	{
		public Vector3 pointA;
		public Vector3 pointB;

		public EdgeInfo(Vector3 _pointA, Vector3 _pointB)
		{
			pointA = _pointA;
			pointB = _pointB;
		}
	}
}
using UnityEditor;

[CustomEditor (typeof (FieldOfView))]
public class FieldOfViewEditor : Editor
{
	private void OnSceneGUI()
	{
		FieldOfView fov = (FieldOfView)target;
		Handles.color = Color.white;
		Handles.DrawWireArc(fov.transform.position, Vector3.up, Vector3.forward, 360, fov.ViewRadius);

		Vector3 viewAngleA = fov.DirFromAngle(-fov.ViewAngle / 2, false);
		Vector3 viewAngleB = fov.DirFromAngle(fov.ViewAngle / 2, false);

		Handles.DrawLine(fov.transform.position, fov.transform.position + viewAngleA * fov.ViewRadius);
		Handles.DrawLine(fov.transform.position, fov.transform.position + viewAngleB  * fov.ViewRadius);

		Handles.color = Color.red;
		foreach (Transform visibleTarget in fov.VisibleTargets)
			Handles.DrawLine(fov.transform.position, visibleTarget.position);
	}
}

Sound Barrier

Summary
What I created here is a sound barrier for the player that can be pickup up by the AI. This gives the AI the players position and makes it more challenging for the player because he has to be silent. I created this script myself and it's fully customizable.

Sound Barrier
This SoundBarrier script creates a 2d circle around the player that grows bigger walking and multiplies when sprinting. This is a fun way for the player to play around with how fast he can go through the map and when to stay silent.

It's hard to see in the videos but there is a blue line from the AI to the player to indicate where the AI is moving towards. So when the green circle(sound barrier) hit the AI the player enters the AI when that happens the AI script updates with a delay so it doesn't update getcomponent every frame and changes the AI GoToPos to the players Position. This works like a charm and can be modified in multiple ways, such as adding effect when you as player gets heard or and an effect or UI effect to the player so it knows to be silent.

using UnityEditor;

public class SoundBarrier : MonoBehaviour
{
	private AgentMovement agent;

	public List<Transform> EnemyHeard = new List<Transform>();

    [Header("Noise Info")]
    [Range(0, 20)]
    [SerializeField] private float currentNoise;

    [Space(20)]
    [Header("Noise Settings")]
    [SerializeField] private float MaxWalkNoise = 10;
    [SerializeField] private float MaxRunNoise = 20;

    [SerializeField] private LayerMask enemyLayer;

	void Start()
    {
        agent = GetComponentInParent<AgentMovement>();
        StartCoroutine("FindTargetsWithDelay", 0.2f);

    }

	void Update()
    {
        if (agent.currentVelocity > 0 && agent.currentVelocity <= 2.5f)
        {
            if (currentNoise >= MaxWalkNoise)
                currentNoise = MaxWalkNoise;

            if (currentNoise <= MaxWalkNoise)
                currentNoise += 0.2f * agent.currentVelocity * 4 * Time.deltaTime;
        }
		else if (agent.currentVelocity > 2.5f && agent.currentVelocity <= 8)
        {
            if (currentNoise <= MaxRunNoise)
                currentNoise += 0.2f * agent.currentVelocity * 4 * Time.deltaTime;
        }
		else if (agent.currentVelocity <= 0)
            if (currentNoise >= 0)
                currentNoise -= 1 * Time.maximumDeltaTime;
    }

	private void NoiseHit()
    {
        EnemyHeard.Clear();
        Collider[] enemy = Physics.OverlapSphere(transform.position, currentNoise, enemyLayer);

        for (int i = 0; i < enemy.Length; i++)
        {
            Transform target = enemy[i].transform;
            Vector3 dirToTarget = (target.position - transform.position).normalized;
            if (Vector3.Angle(transform.forward, dirToTarget) < 360 / 2)
            {
                Debug.Log(target.name);

                EnemyHeard.Add(target);
                for (int a = 0; a < EnemyHeard.Count; a++)
                {
                    EnemyHeard[a].GetComponent<AIMovement>().GoToPos = transform.position;
                    EnemyHeard[a].GetComponent<AIMovement>().state = AgentState.find;
                }
            }
        }
    }

	private void OnDrawGizmos()
    {
        Handles.color = Color.green;
        Handles.DrawSolidArc(transform.position, Vector3.down, Vector3.right, 360, currentNoise);
    }

    IEnumerator FindTargetsWithDelay(float delay)
    {
		while (true)
        {
			yield return new WaitForSeconds(delay);
			NoiseHit();
        }
    }
}